在前面的章節中,我們了解了 AutoFixture 的基本使用方式。今天我們要深入瞭解如何根據業務需求客製化測試資料生成邏輯,學習進階的自訂化技術,讓測試資料更符合實際需求。
AutoFixture 具備自動識別 DataAnnotation Attribute 的能力,能夠產生符合限制的測試資料。這個特性讓我們可以利用現有的模型驗證規則來約束測試資料的生成。
首先,我們建立一個包兩種 DataAnnotation 屬性的 Person 類別:
using System.ComponentModel.DataAnnotations;
public class Person
{
public Guid Id { get; set; }
[StringLength(10)]
public string Name { get; set; } = string.Empty;
[Range(10, 80)]
public int Age { get; set; }
public DateTime CreateTime { get; set; }
}
讓我們測試 AutoFixture 是否能自動識別這些屬性並產生符合限制的資料:
[Fact]
public void AutoFixture_應能識別DataAnnotations並產生符合限制的資料()
{
// Arrange
var fixture = new Fixture();
// Act
var person = fixture.Create<Person>();
// Assert
person.Id.Should().NotBe(Guid.Empty);
person.Name.Should().NotBeNull();
person.Name.Length.Should().Be(10); // StringLength(10) 的限制
person.Age.Should().BeInRange(10, 80); // Range(10, 80) 的限制
person.CreateTime.Should().BeAfter(DateTime.MinValue);
}
我們也可以測試批量產生的物件是否都符合 DataAnnotation 的限制:
[Fact]
public void AutoFixture_批量產生的Person物件_都應符合DataAnnotations限制()
{
// Arrange
var fixture = new Fixture();
// Act
var persons = fixture.CreateMany<Person>(10).ToList();
// Assert
persons.Should().HaveCount(10);
persons.Should().AllSatisfy(person =>
{
person.Name.Length.Should().Be(10);
person.Age.Should().BeInRange(10, 80);
});
}
除了使用 DataAnnotations,我們也可以透過 .With()
方法搭配 Random.Shared.Next()
來動態產生指定範圍的隨機數值。
在 Age 屬性上使用 Range 特性,並限制範圍為 10 ~ 80 之間
public class Member
{
public string Name { get; set; } = string.Empty;
[Range(10, 80)]
public int Age { get; set; }
public string Email { get; set; } = string.Empty;
}
[Fact]
public void 使用With方法_控制Member年齡範圍()
{
// Arrange
var fixture = new Fixture();
// Act
var member = fixture.Build<Member>()
.With(x => x.Age, () => Random.Shared.Next(30, 50))
.Create();
// Assert
member.Age.Should().BeInRange(30, 50);
member.Name.Should().NotBeNull();
member.Email.Should().NotBeNull();
}
當我們需要產生多個物件時,每個物件的屬性值都會重新計算:
[Fact]
public void CreateMany產生多個Member_每個Age都在指定範圍內()
{
// Arrange
var fixture = new Fixture();
// Act
var members = fixture.Build<Member>()
.With(x => x.Age, () => Random.Shared.Next(30, 50))
.CreateMany(10)
.ToList();
// Assert
members.Should().HaveCount(10);
members.Should().AllSatisfy(member =>
{
member.Age.Should().BeInRange(30, 50);
});
// 驗證年齡確實是隨機的(不是所有物件都有相同年齡)
var distinctAges = members.Select(m => m.Age).Distinct().Count();
distinctAges.Should().BeGreaterThan(1);
}
在使用 .With()
方法時,固定值和動態值有重要的差異:
.With(x => x.Prop, value)
:固定值,所有物件都用這個值.With(x => x.Prop, () => value)
:動態值,每個物件都會執行一次 lambda讓我們用實際的範例來展示這個差異:
[Fact]
public void With方法_固定值vs動態值的差異()
{
// Arrange
var fixture = new Fixture();
// Act - 使用固定值(不用 lambda,只會執行一次)
var membersWithFixedAge = fixture.Build<Member>()
.With(x => x.Age, Random.Shared.Next(30, 50)) // 固定值,只執行一次隨機產生,所有物件都是同樣的值
.CreateMany(5)
.ToList();
// Act - 使用動態值(用 lambda,每個物件都會執行一次)
var membersWithDynamicAge = fixture.Build<Member>()
.With(x => x.Age, () => Random.Shared.Next(30, 50)) // 動態值,每個物件都重新計算
.CreateMany(5)
.ToList();
// Assert
// 固定值:所有物件的年齡都相同(雖然是隨機產生的,但只產生一次)
var firstAge = membersWithFixedAge.First().Age;
firstAge.Should().BeInRange(30, 49);
membersWithFixedAge.Should().AllSatisfy(member => member.Age.Should().Be(firstAge));
var distinctFixedAges = membersWithFixedAge.Select(m => m.Age).Distinct().Count();
distinctFixedAges.Should().Be(1); // 只有一個不同的值
// 動態值:每個物件的年齡都可能不同
membersWithDynamicAge.Should().AllSatisfy(member => member.Age.Should().BeInRange(30, 49));
var distinctDynamicAges = membersWithDynamicAge.Select(m => m.Age).Distinct().Count();
distinctDynamicAges.Should().BeGreaterThan(1); // 通常會有多個不同的值
}
在 .NET 6 之後,建議使用 Random.Shared
而不是 new Random()
:
避免重複值問題:如果在短時間內多次呼叫 new Random()
,由於它們可能使用相同的時間種子,會導致產生的亂數值重複。Random.Shared
則不會有這個問題。
效能更好:不需要每次都建立新的 Random 實例,減少資源消耗。
執行緒安全:在多執行緒環境中使用 Random.Shared
是安全的,而 new Random()
則可能導致 race condition 或錯誤的亂數行為。
特性 | new Random() |
Random.Shared |
---|---|---|
實例化方式 | 每次呼叫都會建立新的 Random 實例 |
使用全域共用的單一實例 |
執行緒安全 | 不是執行緒安全 | 是執行緒安全 |
效能 | 多次建立會有效能負擔,可能產生重複值 | 效能更佳,避免重複值問題 |
用途建議 | 適合單執行緒、短期用途 | 適合多執行緒、全域共用用途 |
[Fact]
public void 展示Random共用實例的優勢()
{
// Arrange
var fixture = new Fixture();
// Act - 使用 Random.Shared(推薦)
var members1 = fixture.Build<Member>()
.With(x => x.Age, () => Random.Shared.Next(20, 60))
.CreateMany(20)
.ToList();
// Assert
members1.Should().AllSatisfy(member => member.Age.Should().BeInRange(20, 60));
// 驗證產生的年齡有足夠的隨機性
var distinctAges = members1.Select(m => m.Age).Distinct().Count();
distinctAges.Should().BeGreaterThan(5); // 在 20 個物件中,應該有超過 5 個不同的年齡值
}
對於 DateTime 屬性,AutoFixture 提供了 RandomDateTimeSequenceGenerator
來控制日期範圍。
RandomDateTimeSequenceGenerator
是 AutoFixture 中的一個內建類別,實作了 ISpecimenBuilder 介面,專門用來產生一系列隨機但遞增的 DateTime 值。
功能簡介:
首先,讓我們擴展 Member 類別來包含日期屬性:
public class Member
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public string Email { get; set; } = string.Empty;
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
}
[Fact]
public void 使用RandomDateTimeSequenceGenerator_控制DateTime範圍()
{
// Arrange
var fixture = new Fixture();
var minDate = new DateTime(2025, 1, 1);
var maxDate = new DateTime(2025, 12, 31);
fixture.Customizations.Add(new RandomDateTimeSequenceGenerator(minDate, maxDate));
// Act
var member = fixture.Create<Member>();
// Assert
member.CreateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
member.UpdateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
}
RandomDateTimeSequenceGenerator
會影響所有的 DateTime 屬性,這可能不是我們想要的結果。有時候我們希望只控制特定的屬性。
[Fact]
public void RandomDateTimeSequenceGenerator_會影響所有DateTime屬性()
{
// Arrange
var fixture = new Fixture();
var minDate = new DateTime(2025, 1, 1);
var maxDate = new DateTime(2025, 12, 31);
fixture.Customizations.Add(new RandomDateTimeSequenceGenerator(minDate, maxDate));
// Act
var members = fixture.CreateMany<Member>(5).ToList();
// Assert
members.Should().AllSatisfy(member =>
{
// 所有的 DateTime 屬性都會被限制在同樣的範圍內
member.CreateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
member.UpdateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
});
}
使用 RandomDateTimeSequenceGenerator 要注意的事情
用了 RandomDateTimeSequenceGenerator 這個後,會影響到同個類別的其他相同 DateTime 屬性。只要它們是透過 AutoFixture 自動產生的,且沒有被其他自訂邏輯 (例如 .With (...)) 覆蓋的話,就會受到影響。
RandomDateTimeSequenceGenerator 是一個實作 ISpecimenBuilder
介面的類別,所以當我們在測試方法裡有作了以下的設定
fixture.Customizations.Add(new RandomDateTimeSequenceGenerator());
它會攔截所有 DateTime 型別的請求,並回傳一個隨機但遞增的 DateTime 值。因此:
如何避免影響其他屬性?
可以自己繼承 ISpecimenBuilder
介面並實作一個可以指定屬性的 RandomRangedDateTimeBuilder
類別,這個類別可以讓我們去指定要產生指定範圍日期時間的屬性名稱,這樣就不會取影響到其他同樣是 DateTime 型別的屬性了。
using AutoFixture.Kernel;
using System.Reflection;
public class RandomRangedDateTimeBuilder : ISpecimenBuilder
{
private readonly DateTime _minDate;
private readonly DateTime _maxDate;
private readonly HashSet<string> _targetProperties;
public RandomRangedDateTimeBuilder(DateTime minDate, DateTime maxDate, params string[] targetProperties)
{
_minDate = minDate;
_maxDate = maxDate;
_targetProperties = new HashSet<string>(targetProperties);
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(DateTime) &&
_targetProperties.Contains(propertyInfo.Name))
{
var range = _maxDate - _minDate;
var randomTicks = (long)(Random.Shared.NextDouble() * range.Ticks);
return _minDate.AddTicks(randomTicks);
}
return new NoSpecimen();
}
}
[Fact]
public void 使用自訂RandomRangedDateTimeBuilder_只控制特定DateTime屬性()
{
// Arrange
var fixture = new Fixture();
var minDate = new DateTime(2025, 1, 1);
var maxDate = new DateTime(2025, 12, 31);
// 使用 RandomRangedDateTimeBuilder 控制 UpdateTime 屬性
fixture.Customizations.Add(new RandomRangedDateTimeBuilder(minDate, maxDate, "UpdateTime"));
var otherMinDate = new DateTime(2024, 1, 1);
var otherMaxDate = new DateTime(2024, 12, 31);
// 使用 RandomDateTimeSequenceGenerator 控制其他 DateTime 屬性
fixture.Customizations.Add(new RandomDateTimeSequenceGenerator(otherMinDate, otherMaxDate));
// Act
var member = fixture.Create<Member>();
// Assert
// UpdateTime 應該在 2024-01-01 ~ 2024-12-31 之間
member.UpdateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
(member.UpdateTime >= minDate && member.UpdateTime <= maxDate).Should().BeTrue();
(member.UpdateTime < otherMinDate || member.UpdateTime > otherMaxDate).Should().BeTrue();
// CreateTime 應該在 2025-01-01 ~ 2025-12-31 之間
member.CreateTime.Should().BeOnOrAfter(otherMinDate).And.BeOnOrBefore(otherMaxDate);
(member.CreateTime >= otherMinDate && member.CreateTime <= otherMaxDate).Should().BeTrue();
(member.CreateTime < minDate || member.CreateTime > maxDate).Should().BeTrue();
}
可以看到 UpdateTime 的值都在指定的日期時間範圍內,而 CreateTime 則不受到影響。
NoSpecimen
是 AutoFixture 中的特殊物件,表示目前的建構器無法處理此請求,應該交由責任鏈中的下一個建構器處理:
[Fact]
public void RandomRangedDateTimeBuilder_應正確回傳NoSpecimen()
{
// Arrange
var minDate = new DateTime(2025, 1, 1);
var maxDate = new DateTime(2025, 12, 31);
var builder = new RandomRangedDateTimeBuilder(minDate, maxDate, "CreateTime");
var context = new SpecimenContext(new Fixture());
// Act & Assert - 非目標屬性應回傳 NoSpecimen
var stringPropertyRequest = typeof(Member).GetProperty("Name");
var result1 = builder.Create(stringPropertyRequest, context);
result1.Should().BeOfType<NoSpecimen>();
// DateTime 但非目標屬性也應回傳 NoSpecimen
var updateTimePropertyRequest = typeof(Member).GetProperty("UpdateTime");
var result2 = builder.Create(updateTimePropertyRequest, context);
result2.Should().BeOfType<NoSpecimen>();
// 目標屬性應回傳 DateTime
var createTimePropertyRequest = typeof(Member).GetProperty("CreateTime");
var result3 = builder.Create(createTimePropertyRequest, context);
result3.Should().BeOfType<DateTime>();
}
對於數值屬性的範圍控制,情況比 DateTime 更複雜,因為 AutoFixture 對 int 和 DateTime 有不同的處理方式。
讓我們先嘗試一個簡單的實作:
public class RandomRangedNumericSequenceBuilder : ISpecimenBuilder
{
private readonly int _min;
private readonly int _max;
private readonly HashSet<string> _targetProperties;
public RandomRangedNumericSequenceBuilder(int min, int max, params string[] targetProperties)
{
_min = min;
_max = max;
_targetProperties = new HashSet<string>(targetProperties);
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(int) &&
_targetProperties.Contains(propertyInfo.Name))
{
return Random.Shared.Next(_min, _max);
}
return new NoSpecimen();
}
}
這個簡單的實作在某些情況下可能會失效:
[Fact]
public void 第一個版本RandomRangedNumericSequenceBuilder_可能會失效()
{
// Arrange
var fixture = new Fixture();
fixture.Customizations.Add(new RandomRangedNumericSequenceBuilder(30, 50, "Age"));
// Act
var member = fixture.Create<Member>();
// Assert
// 這個測試展示第一個版本的問題:
// 由於 AutoFixture 內建建構器的優先順序更高,我們的自訂建構器可能會失效
// 結果可能還是使用 DataAnnotations 的範圍(10-80)而不是我們指定的範圍(30-50)
member.Age.Should().BeInRange(10, 80); // 實際上還是使用 DataAnnotations 的範圍
// 驗證不在我們指定的範圍內,說明自訂建構器失效了
if (member.Age is < 30 or >= 50)
{
// 這證明了第一個版本的問題:內建建構器優先順序更高
member.Age.Should().BeInRange(10, 80); // 使用 DataAnnotations 的範圍
}
}
AutoFixture 內建了許多建構器,如 RangeAttributeRelay
、NumericSequenceGenerator
等,它們可能比我們的自訂建構器有更高的優先順序。
int 和 DateTime 在 AutoFixture 的預設產生流程中,其實走的是不同的行為:
整數 (int) 的預設生成器
RangeAttributeRelay
、NumericSequenceGenerator
日期時間 (DateTime) 的預設生成器
RangeAttributeRelay
或序列產生器,只有最後才跑的 ReflectionRelay
(透過 ctor 建物件、屬性注入)。為了解決優先順序問題,我們需要更精確的控制:
public class ImprovedRandomRangedNumericSequenceBuilder : ISpecimenBuilder
{
private readonly int _min;
private readonly int _max;
private readonly Func<PropertyInfo, bool> _predicate;
public ImprovedRandomRangedNumericSequenceBuilder(int min, int max, Func<PropertyInfo, bool> predicate)
{
_min = min;
_max = max;
_predicate = predicate;
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(int) &&
_predicate(propertyInfo))
{
return Random.Shared.Next(_min, _max);
}
return new NoSpecimen();
}
}
關鍵在於使用 Insert(0)
而不是 Add()
,這樣可以確保我們的建構器有最高的優先順序:
[Fact]
public void 使用Insert0確保自訂建構器優先順序_控制Member年齡範圍()
{
// Arrange
var fixture = new Fixture();
// 使用 Insert(0) 確保最高優先順序
fixture.Customizations.Insert(0, new ImprovedRandomRangedNumericSequenceBuilder(
30, 50,
prop => prop.Name == "Age" && prop.DeclaringType == typeof(Member)));
// Act
var member = fixture.Create<Member>();
// Assert
member.Age.Should().BeInRange(30, 49);
}
[Fact]
public void ImprovedRandomRangedNumericSequenceBuilder_完整測試案例()
{
// Arrange
var fixture = new Fixture();
fixture.Customizations.Insert(0, new ImprovedRandomRangedNumericSequenceBuilder(
30, 50,
prop => prop.Name == "Age" && prop.DeclaringType == typeof(Member)));
// Act
var members = fixture.CreateMany<Member>(20).ToList();
// Assert
members.Should().HaveCount(20);
members.Should().AllSatisfy(member =>
{
member.Age.Should().BeInRange(30, 49);
});
// 驗證年齡確實是隨機的
var distinctAges = members.Select(m => m.Age).Distinct().Count();
distinctAges.Should().BeGreaterThan(1);
}
最後,我們要建立一個支援所有數值型別的泛型化設計,讓同一套機制可以處理各種數值型別。
我們需要支援的數值型別包括:
public class NumericRangeBuilder<TValue> : ISpecimenBuilder
where TValue : struct, IComparable, IConvertible
{
private readonly TValue _min;
private readonly TValue _max;
private readonly Func<PropertyInfo, bool> _predicate;
public NumericRangeBuilder(TValue min, TValue max, Func<PropertyInfo, bool> predicate)
{
_min = min;
_max = max;
_predicate = predicate;
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(TValue) &&
_predicate(propertyInfo))
{
return GenerateRandomValue();
}
return new NoSpecimen();
}
private TValue GenerateRandomValue()
{
// 統一轉換為 decimal 進行計算
var minDecimal = Convert.ToDecimal(_min);
var maxDecimal = Convert.ToDecimal(_max);
var range = maxDecimal - minDecimal;
var randomValue = minDecimal + (decimal)Random.Shared.NextDouble() * range;
// 根據目標型別轉換回去
return typeof(TValue).Name switch
{
nameof(Int32) => (TValue)(object)(int)randomValue,
nameof(Int64) => (TValue)(object)(long)randomValue,
nameof(Int16) => (TValue)(object)(short)randomValue,
nameof(Byte) => (TValue)(object)(byte)randomValue,
nameof(Single) => (TValue)(object)(float)randomValue,
nameof(Double) => (TValue)(object)(double)randomValue,
nameof(Decimal) => (TValue)(object)randomValue,
_ => throw new NotSupportedException($"Type {typeof(TValue).Name} is not supported")
};
}
}
為了讓使用更方便,我們建立擴充方法:
public static class FixtureRangedNumericExtensions
{
public static IFixture AddRandomRange<T, TValue>(this IFixture fixture,
TValue min, TValue max, Func<PropertyInfo, bool> predicate)
where TValue : struct, IComparable, IConvertible
{
fixture.Customizations.Insert(0, new NumericRangeBuilder<TValue>(min, max, predicate));
return fixture;
}
}
[Fact]
public void 使用擴充方法_控制不同數值型別的範圍()
{
// Arrange
var fixture = new Fixture();
// 控制不同型別的屬性範圍
fixture.AddRandomRange<Product, decimal>(
min: 100m,
max: 1000m,
predicate: prop => prop.Name == "Price" && prop.DeclaringType == typeof(Product));
fixture.AddRandomRange<Product, int>(
min: 1,
max: 100,
predicate: prop => prop.Name == "Quantity" && prop.DeclaringType == typeof(Product));
// Act
var product = fixture.Create<Product>();
// Assert
product.Price.Should().BeInRange(100m, 1000m);
product.Quantity.Should().BeInRange(1, 99); // Next 不包含上限
}
定義包含多種數值型別的複雜實體:
public class Product
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
public double Rating { get; set; }
public float Discount { get; set; }
}
public class Order
{
public string OrderNumber { get; set; } = string.Empty;
public decimal Amount { get; set; }
public short ItemCount { get; set; }
public byte Priority { get; set; }
public long CustomerId { get; set; }
}
[Fact]
public void 複雜實體多重數值型別範圍控制_完整測試案例()
{
// Arrange
var fixture = new Fixture();
// Product 屬性範圍設定
fixture.AddRandomRange<Product, decimal>(
min: 50m,
max: 500m,
predicate: prop => prop.Name == "Price" && prop.DeclaringType == typeof(Product));
fixture.AddRandomRange<Product, int>(
min: 1,
max: 50,
predicate: prop => prop.Name == "Quantity" && prop.DeclaringType == typeof(Product));
fixture.AddRandomRange<Product, double>(
min: 1.0,
max: 5.0,
predicate: prop => prop.Name == "Rating" && prop.DeclaringType == typeof(Product));
fixture.AddRandomRange<Product, float>(
min: 0.0f,
max: 0.5f,
predicate: prop => prop.Name == "Discount" && prop.DeclaringType == typeof(Product));
// Order 屬性範圍設定
fixture.AddRandomRange<Order, decimal>(
min: 100m,
max: 10000m,
predicate: prop => prop.Name == "Amount" && prop.DeclaringType == typeof(Order));
fixture.AddRandomRange<Order, short>(
min: (short)1,
max: (short)20,
predicate: prop => prop.Name == "ItemCount" && prop.DeclaringType == typeof(Order));
fixture.AddRandomRange<Order, byte>(
min: (byte)1,
max: (byte)5,
predicate: prop => prop.Name == "Priority" && prop.DeclaringType == typeof(Order));
fixture.AddRandomRange<Order, long>(
min: 1000L,
max: 9999L,
predicate: prop => prop.Name == "CustomerId" && prop.DeclaringType == typeof(Order));
// Act
var products = fixture.CreateMany<Product>(10).ToList();
var orders = fixture.CreateMany<Order>(10).ToList();
// Assert
products.Should().AllSatisfy(product =>
{
product.Price.Should().BeInRange(50m, 500m);
product.Quantity.Should().BeInRange(1, 49);
product.Rating.Should().BeInRange(1.0, 5.0);
product.Discount.Should().BeInRange(0.0f, 0.5f);
});
orders.Should().AllSatisfy(order =>
{
order.Amount.Should().BeInRange(100m, 10000m);
order.ItemCount.Should().BeInRange((short)1, (short)19);
order.Priority.Should().BeInRange((byte)1, (byte)4);
order.CustomerId.Should().BeInRange(1000L, 9998L);
});
}
善用 DataAnnotations 整合
適當的屬性範圍控制
.With()
方法配合 Random.Shared.Next()
動態產生隨機值自訂建構器的優先順序管理
Insert(0)
而不是 Add()
確保自訂建構器優先權泛型化設計的運用
忽略建構器優先順序
Add()
的自訂建構器一定會生效過度複雜的自訂邏輯
循環參考處理不當
今天我們深入探討了 AutoFixture 的進階自訂化功能,從基礎的 DataAnnotations 整合到複雜的泛型數值範圍建構器。這些技術展現了 AutoFixture 不僅能自動產生測試資料,更能精確控制資料產生的邏輯,讓測試資料更符合實際業務需求。
.With()
方法和 Random.Shared
實現動態且可重複的屬性值產生RandomDateTimeSequenceGenerator
到精確的 RandomRangedDateTimeBuilder
RandomRangedNumericSequenceBuilder
使用屬性名稱匹配的簡單方式ImprovedRandomRangedNumericSequenceBuilder
採用條件函數提供更靈活的判斷機制NumericRangeBuilder<TValue>
支援所有數值型別,實現真正的可重用性FixtureRangedNumericExtensions
提供優雅的擴充方法語法ISpecimenBuilder
介面實現可插拔的建構器架構Insert(0)
確保優先順序符合預期這些進階技術讓我們能夠:
在 Day 10 介紹了 AutoFixture 的基本用法,然後今天則深入了自訂建構器和泛型化設計。從基礎的資料產生到精確的範圍控制,我們掌握了更靈活的測試資料準備技術。
明天我們將學習 AutoData 屬性與 xUnit 的整合,透過屬性標註讓測試參數自動產生,減少更多的準備程式碼。
範例程式碼:
這是「重啟挑戰:老派軟體工程師的測試修練」的第十一天。明天會介紹 Day 12 – 結合 AutoData:xUnit 與 AutoFixture 的整合應用。